/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.bmp;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.apache.sanselan.FormatCompliance;
import org.apache.sanselan.ImageFormat;
import org.apache.sanselan.ImageInfo;
import org.apache.sanselan.ImageParser;
import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.common.BinaryOutputStream;
import org.apache.sanselan.common.IImageMetadata;
import org.apache.sanselan.common.byteSources.ByteSource;
import org.apache.sanselan.formats.bmp.pixelparsers.PixelParser;
import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserBitFields;
import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserRgb;
import org.apache.sanselan.formats.bmp.pixelparsers.PixelParserRle;
import org.apache.sanselan.formats.bmp.writers.BMPWriter;
import org.apache.sanselan.formats.bmp.writers.BMPWriterPalette;
import org.apache.sanselan.formats.bmp.writers.BMPWriterRGB;
import org.apache.sanselan.palette.PaletteFactory;
import org.apache.sanselan.palette.SimplePalette;
import org.apache.sanselan.util.Debug;
import org.apache.sanselan.util.ParamMap;

public class BmpImageParser extends ImageParser
{

	public BmpImageParser()
	{
		super.setByteOrder(BYTE_ORDER_INTEL);
	}

	public String getName()
	{
		return "Bmp-Custom";
	}

	public String getDefaultExtension()
	{
		return DEFAULT_EXTENSION;
	}

	private static final String DEFAULT_EXTENSION = ".bmp";

	private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, };

	protected String[] getAcceptedExtensions()
	{
		return ACCEPTED_EXTENSIONS;
	}

	protected ImageFormat[] getAcceptedTypes()
	{
		return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_BMP, //
		};
	}

	private static final byte BMP_HEADER_SIGNATURE[] = { 0x42, 0x4d, };

	private BmpHeaderInfo readBmpHeaderInfo(InputStream is,
			FormatCompliance formatCompliance, boolean verbose)
			throws ImageReadException, IOException
	{
		byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File");
		byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File");

		if (formatCompliance != null)
		{
			formatCompliance.compare_bytes("Signature", BMP_HEADER_SIGNATURE,
					new byte[] { identifier1, identifier2, });
		}

		int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File");
		int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File");
		int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is,
				"Not a Valid BMP File");

		int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is,
				"Not a Valid BMP File");
		int width = read4Bytes("Width", is, "Not a Valid BMP File");
		int height = read4Bytes("Height", is, "Not a Valid BMP File");
		int planes = read2Bytes("Planes", is, "Not a Valid BMP File");
		int bitsPerPixel = read2Bytes("Bits Per Pixel", is,
				"Not a Valid BMP File");
		int compression = read4Bytes("Compression", is, "Not a Valid BMP File");
		int bitmapDataSize = read4Bytes("Bitmap Data Size", is,
				"Not a Valid BMP File");
		int hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File");
		int vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File");
		int colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File");
		int colorsImportant = read4Bytes("ColorsImportant", is,
				"Not a Valid BMP File");

		if (verbose)
		{
			this.debugNumber("identifier1", identifier1, 1);
			this.debugNumber("identifier2", identifier2, 1);
			this.debugNumber("fileSize", fileSize, 4);
			this.debugNumber("reserved", reserved, 4);
			this.debugNumber("bitmapDataOffset", bitmapDataOffset, 4);
			this.debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4);
			this.debugNumber("width", width, 4);
			this.debugNumber("height", height, 4);
			this.debugNumber("planes", planes, 2);
			this.debugNumber("bitsPerPixel", bitsPerPixel, 2);
			this.debugNumber("compression", compression, 4);
			this.debugNumber("bitmapDataSize", bitmapDataSize, 4);
			this.debugNumber("hResolution", hResolution, 4);
			this.debugNumber("vResolution", vResolution, 4);
			this.debugNumber("colorsUsed", colorsUsed, 4);
			this.debugNumber("colorsImportant", colorsImportant, 4);
		}

		BmpHeaderInfo result = new BmpHeaderInfo(identifier1, identifier2,
				fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width,
				height, planes, bitsPerPixel, compression, bitmapDataSize,
				hResolution, vResolution, colorsUsed, colorsImportant);
		return result;
	}

	private static final int BI_RGB = 0;
	private static final int BI_RLE4 = 2;
	private static final int BI_RLE8 = 1;
	private static final int BI_BITFIELDS = 3;

	private byte[] getRLEBytes(InputStream is, int RLESamplesPerByte)
			throws ImageReadException, IOException
	{
		ByteArrayOutputStream baos = new ByteArrayOutputStream();

		// this.setDebug(true);

		boolean done = false;
		while (!done)
		{
			int a = 0xff & this.readByte("RLE a", is, "BMP: Bad RLE");
			baos.write(a);
			int b = 0xff & this.readByte("RLE b", is, "BMP: Bad RLE");
			baos.write(b);

			if (a == 0)
			{
				switch (b)
				{
				case 0: // EOL
					break;
				case 1: // EOF
					// System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
					// );
					done = true;
					break;
				case 2: {
					// System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
					// );
					int c = 0xff & this.readByte("RLE c", is, "BMP: Bad RLE");
					baos.write(c);
					int d = 0xff & this.readByte("RLE d", is, "BMP: Bad RLE");
					baos.write(d);

				}
					break;
				default: {
					int size = b / RLESamplesPerByte;
					if ((b % RLESamplesPerByte) > 0)
						size++;
					if ((size % 2) != 0)
						size++;

					// System.out.println("b: " + b);
					// System.out.println("size: " + size);
					// System.out.println("RLESamplesPerByte: " +
					// RLESamplesPerByte);
					// System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
					// );
					byte bytes[] = this.readByteArray("bytes", size, is,
							"RLE: Absolute Mode");
					baos.write(bytes);
				}
					break;
				}
			}
		}

		return baos.toByteArray();
	}

	private ImageContents readImageContents(InputStream is,
			FormatCompliance formatCompliance, boolean verbose)
			throws ImageReadException, IOException
	{
		BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance, verbose);

		int colorTableSize = bhi.colorsUsed;
		if (colorTableSize == 0)
			colorTableSize = (1 << bhi.bitsPerPixel);

		if (verbose)
		{
			this.debugNumber("ColorsUsed", bhi.colorsUsed, 4);
			this.debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4);
			this.debugNumber("ColorTableSize", colorTableSize, 4);
			this.debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4);
			this.debugNumber("Compression", bhi.compression, 4);
		}

		int paletteLength;
		int rleSamplesPerByte = 0;
		boolean rle = false;

		switch (bhi.compression)
		{
		case BI_RGB:
			if (verbose)
				System.out.println("Compression: BI_RGB");
			if (bhi.bitsPerPixel <= 8)
				paletteLength = 4 * colorTableSize;
			else
				paletteLength = 0;
			// BytesPerPaletteEntry = 0;
			// System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel);
			// System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel
			// <= 16));
			break;

		case BI_RLE4:
			if (verbose)
				System.out.println("Compression: BI_RLE4");
			paletteLength = 4 * colorTableSize;
			rleSamplesPerByte = 2;
			// ExtraBitsPerPixel = 4;
			rle = true;
			// // BytesPerPixel = 2;
			// // BytesPerPaletteEntry = 0;
			break;
		//
		case BI_RLE8:
			if (verbose)
				System.out.println("Compression: BI_RLE8");
			paletteLength = 4 * colorTableSize;
			rleSamplesPerByte = 1;
			// ExtraBitsPerPixel = 8;
			rle = true;
			// BytesPerPixel = 2;
			// BytesPerPaletteEntry = 0;
			break;
		//
		case BI_BITFIELDS:
			if (verbose)
				System.out.println("Compression: BI_BITFIELDS");
			paletteLength = 3 * 4; // TODO: is this right? are the masks always
			// LONGs?
			// BytesPerPixel = 2;
			// BytesPerPaletteEntry = 4;
			break;

		default:
			throw new ImageReadException("BMP: Unknown Compression: "
					+ bhi.compression);
		}

		byte colorTable[] = null;
		if (paletteLength > 0)
			colorTable = this.readByteArray("ColorTable", paletteLength, is,
					"Not a Valid BMP File");

		if (verbose)
		{
			this.debugNumber("paletteLength", paletteLength, 4);
			System.out.println("ColorTable: "
					+ ((colorTable == null) ? "null" : "" + colorTable.length));
		}

		int pixelCount = bhi.width * bhi.height;

		int imageLineLength = ((((bhi.bitsPerPixel) * bhi.width) + 7) / 8);

		if (verbose)
		{
			// this.debugNumber("Total BitsPerPixel",
			// (ExtraBitsPerPixel + bhi.BitsPerPixel), 4);
			// this.debugNumber("Total Bit Per Line",
			// ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4);
			// this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4);
			this.debugNumber("bhi.Width", bhi.width, 4);
			this.debugNumber("bhi.Height", bhi.height, 4);
			this.debugNumber("ImageLineLength", imageLineLength, 4);
			// this.debugNumber("imageDataSize", imageDataSize, 4);
			this.debugNumber("PixelCount", pixelCount, 4);
		}
		// int ImageLineLength = BytesPerPixel * bhi.Width;
		while ((imageLineLength % 4) != 0)
			imageLineLength++;

		final int headerSize = BITMAP_FILE_HEADER_SIZE
				+ BITMAP_INFO_HEADER_SIZE;
		int expectedDataOffset = headerSize + paletteLength;

		if (verbose)
		{
			this.debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4);
			this.debugNumber("expectedDataOffset", expectedDataOffset, 4);
		}
		int extraBytes = bhi.bitmapDataOffset - expectedDataOffset;
		if (extraBytes < 0)
			throw new ImageReadException("BMP has invalid image data offset: "
					+ bhi.bitmapDataOffset + " (expected: "
					+ expectedDataOffset + ", paletteLength: " + paletteLength
					+ ", headerSize: " + headerSize + ")");
		else if (extraBytes > 0)
			this.readByteArray("BitmapDataOffset", extraBytes, is,
					"Not a Valid BMP File");

		int imageDataSize = bhi.height * imageLineLength;

		if (verbose)
			this.debugNumber("imageDataSize", imageDataSize, 4);

		byte imageData[];
		if (rle)
			imageData = getRLEBytes(is, rleSamplesPerByte);
		else
			imageData = this.readByteArray("ImageData", imageDataSize, is,
					"Not a Valid BMP File");

		if (verbose)
			this.debugNumber("ImageData.length", imageData.length, 4);

		PixelParser pixelParser;

		switch (bhi.compression)
		{
		case BI_RLE4:
		case BI_RLE8:
			pixelParser = new PixelParserRle(bhi, colorTable, imageData);
			break;
		case BI_RGB:
			pixelParser = new PixelParserRgb(bhi, colorTable, imageData);
			break;
		case BI_BITFIELDS:
			pixelParser = new PixelParserBitFields(bhi, colorTable, imageData);
			break;
		default:
			throw new ImageReadException("BMP: Unknown Compression: "
					+ bhi.compression);
		}

		return new ImageContents(bhi, colorTable, imageData, pixelParser);
	}

	private BmpHeaderInfo readBmpHeaderInfo(ByteSource byteSource,
			boolean verbose) throws ImageReadException, IOException
	{
		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();

			// readSignature(is);
			return readBmpHeaderInfo(is, null, verbose);
		} finally
		{
			try
			{
				is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}

		}
	}

	public byte[] getICCProfileBytes(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		return null;
	}

	public Dimension getImageSize(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		// make copy of params; we'll clear keys as we consume them.
		params = (params == null) ? new HashMap() : new HashMap(params);

		boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE,
				false);

		if (params.containsKey(PARAM_KEY_VERBOSE))
			params.remove(PARAM_KEY_VERBOSE);

		if (params.size() > 0)
		{
			Object firstKey = params.keySet().iterator().next();
			throw new ImageReadException("Unknown parameter: " + firstKey);
		}

		BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource, verbose);

		if (bhi == null)
			throw new ImageReadException("BMP: couldn't read header");

		return new Dimension(bhi.width, bhi.height);

	}

	public byte[] embedICCProfile(byte image[], byte profile[])
	{
		return null;
	}

	public boolean embedICCProfile(File src, File dst, byte profile[])
	{
		return false;
	}

	public IImageMetadata getMetadata(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		return null;
	}

	private String getBmpTypeDescription(int Identifier1, int Identifier2)
	{
		if ((Identifier1 == 'B') && (Identifier2 == 'M'))
			return "Windows 3.1x, 95, NT,";
		if ((Identifier1 == 'B') && (Identifier2 == 'A'))
			return "OS/2 Bitmap Array";
		if ((Identifier1 == 'C') && (Identifier2 == 'I'))
			return "OS/2 Color Icon";
		if ((Identifier1 == 'C') && (Identifier2 == 'P'))
			return "OS/2 Color Pointer";
		if ((Identifier1 == 'I') && (Identifier2 == 'C'))
			return "OS/2 Icon";
		if ((Identifier1 == 'P') && (Identifier2 == 'T'))
			return "OS/2 Pointer";

		return "Unknown";
	}

	public ImageInfo getImageInfo(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		// make copy of params; we'll clear keys as we consume them.
		params = (params == null) ? new HashMap() : new HashMap(params);

		boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE,
				false);

		if (params.containsKey(PARAM_KEY_VERBOSE))
			params.remove(PARAM_KEY_VERBOSE);

		if (params.size() > 0)
		{
			Object firstKey = params.keySet().iterator().next();
			throw new ImageReadException("Unknown parameter: " + firstKey);
		}

		ImageContents ic = readImageContents(byteSource.getInputStream(),
				FormatCompliance.getDefault(), verbose);

		if (ic == null)
			throw new ImageReadException("Couldn't read BMP Data");

		BmpHeaderInfo bhi = ic.bhi;
		byte colorTable[] = ic.colorTable;

		if (bhi == null)
			throw new ImageReadException("BMP: couldn't read header");

		int height = bhi.height;
		int width = bhi.width;

		ArrayList comments = new ArrayList();
		// TODO: comments...

		int bitsPerPixel = bhi.bitsPerPixel;
		ImageFormat format = ImageFormat.IMAGE_FORMAT_BMP;
		String name = "BMP Windows Bitmap";
		String mimeType = "image/x-ms-bmp";
		// we ought to count images, but don't yet.
		int numberOfImages = -1;
		// not accurate ... only reflects first
		boolean isProgressive = false;
		// boolean isProgressive = (fPNGChunkIHDR.InterlaceMethod != 0);
		//
		// pixels per meter
		int physicalWidthDpi = (int) ((double) bhi.hResolution * 1000.0 / 2.54);
		float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
		// int physicalHeightDpi = 72;
		int physicalHeightDpi = (int) ((double) bhi.vResolution * 1000.0 / 2.54);
		float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);

		String formatDetails = "Bmp (" + (char) bhi.identifier1
				+ (char) bhi.identifier2 + ": "
				+ getBmpTypeDescription(bhi.identifier1, bhi.identifier2) + ")";

		boolean isTransparent = false;

		boolean usesPalette = colorTable != null;
		int colorType = ImageInfo.COLOR_TYPE_RGB;
		String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_RLE;

		ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments,
				format, name, height, mimeType, numberOfImages,
				physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
				physicalWidthInch, width, isProgressive, isTransparent,
				usesPalette, colorType, compressionAlgorithm);

		return result;
	}

	public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource)
			throws ImageReadException, IOException
	{
		pw.println("bmp.dumpImageFile");

		ImageInfo imageData = getImageInfo(byteSource, null);
		if (imageData == null)
			return false;

		imageData.toString(pw, "");

		pw.println("");

		return true;
	}

	public FormatCompliance getFormatCompliance(ByteSource byteSource)
			throws ImageReadException, IOException
	{
		boolean verbose = false;

		FormatCompliance result = new FormatCompliance(byteSource
				.getDescription());

		readImageContents(byteSource.getInputStream(), result, verbose);

		return result;
	}

	public BufferedImage getBufferedImage(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		// make copy of params; we'll clear keys as we consume them.
		params = (params == null) ? new HashMap() : new HashMap(params);

		boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE,
				false);

		if (params.containsKey(PARAM_KEY_VERBOSE))
			params.remove(PARAM_KEY_VERBOSE);
		if (params.containsKey(BUFFERED_IMAGE_FACTORY))
			params.remove(BUFFERED_IMAGE_FACTORY);

		if (params.size() > 0)
		{
			Object firstKey = params.keySet().iterator().next();
			throw new ImageReadException("Unknown parameter: " + firstKey);
		}

		ImageContents ic = readImageContents(byteSource.getInputStream(),
				FormatCompliance.getDefault(), verbose);
		if (ic == null)
			throw new ImageReadException("Couldn't read BMP Data");

		BmpHeaderInfo bhi = ic.bhi;
		// byte colorTable[] = ic.colorTable;
		// byte imageData[] = ic.imageData;

		int width = bhi.width;
		int height = bhi.height;

		boolean hasAlpha = false;
		BufferedImage result = getBufferedImageFactory(params)
				.getColorBufferedImage(width, height, hasAlpha);

		if (verbose)
		{
			System.out.println("width: " + width);
			System.out.println("height: " + height);
			System.out.println("width*height: " + width * height);
			System.out.println("width*height*4: " + width * height * 4);
		}

		PixelParser pixelParser = ic.pixelParser;

		pixelParser.processImage(result);

		return result;

	}

	private static final int BITMAP_FILE_HEADER_SIZE = 14;
	private static final int BITMAP_INFO_HEADER_SIZE = 40;

	public void writeImage(BufferedImage src, OutputStream os, Map params)
			throws ImageWriteException, IOException
	{
		// make copy of params; we'll clear keys as we consume them.
		params = (params == null) ? new HashMap() : new HashMap(params);

		// clear format key.
		if (params.containsKey(PARAM_KEY_FORMAT))
			params.remove(PARAM_KEY_FORMAT);

		if (params.size() > 0)
		{
			Object firstKey = params.keySet().iterator().next();
			throw new ImageWriteException("Unknown parameter: " + firstKey);
		}

		final SimplePalette palette = new PaletteFactory().makePaletteSimple(
				src, 256);

		BMPWriter writer = null;
		if (palette == null)
			writer = new BMPWriterRGB();
		else
			writer = new BMPWriterPalette(palette);

		byte imagedata[] = writer.getImageData(src);
		BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_INTEL);

		{
			// write BitmapFileHeader
			os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap
			os.write(0x4d); // M

			int filesize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header
					// size
					4 * writer.getPaletteSize() + // palette size in bytes
					imagedata.length;
			bos.write4Bytes(filesize);

			bos.write4Bytes(0); // reserved
			bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE
					+ 4 * writer.getPaletteSize()); // Bitmap Data Offset
		}

		int width = src.getWidth();
		int height = src.getHeight();

		{ // write BitmapInfoHeader
			bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size
			bos.write4Bytes(width); // width
			bos.write4Bytes(height); // height
			bos.write2Bytes(1); // Number of Planes
			bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel

			bos.write4Bytes(BI_RGB); // Compression
			bos.write4Bytes(imagedata.length); // Bitmap Data Size
			bos.write4Bytes(0); // HResolution
			bos.write4Bytes(0); // VResolution
			if (palette == null)
				bos.write4Bytes(0); // Colors
			else
				bos.write4Bytes(palette.length()); // Colors
			bos.write4Bytes(0); // Important Colors
			// bos.write_4_bytes(0); // Compression
		}

		{ // write Palette
			writer.writePalette(bos);
		}
		{ // write Image Data
			bos.writeByteArray(imagedata);
		}
	}

	/**
	 * Extracts embedded XML metadata as XML string.
	 * <p>
	 * 
	 * @param file
	 *            File containing image data.
	 * @param params
	 *            Map of optional parameters, defined in SanselanConstants.
	 * @return Xmp Xml as String, if present. Otherwise, returns null..
	 */
	public String getXmpXml(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		return null;
	}

}